package com.xayq.orap.service;

import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.collections.CollectionUtils;
import org.apache.oltu.oauth2.as.issuer.OAuthIssuer;
import org.apache.oltu.oauth2.common.OAuth;
import org.apache.oltu.oauth2.common.error.OAuthError;
import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import org.apache.oltu.oauth2.common.message.OAuthResponse;
import org.apache.oltu.oauth2.common.utils.OAuthUtils;
import org.apache.oltu.oauth2.rs.request.OAuthAccessResourceRequest;
import org.apache.oltu.oauth2.rs.response.OAuthRSResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import com.xayq.orap.mapper.ClientMapper;
import com.xayq.orap.mapper.OAuthMapper;
//import com.xayq.orap.mapper.OAuthMapper;
import com.xayq.orap.model.AccessToken;
import com.xayq.orap.model.ClientDetails;
import com.xayq.orap.model.OAuthCode;
import com.xayq.orap.model.User;
import com.xayq.orap.utils.CommonUtils;

@Service
public class OAuthService{

    private static final Logger LOG = LoggerFactory.getLogger(OAuthService.class);


    @Autowired
    private OAuthMapper oAuthMapper;
    
    @Autowired
    private ClientMapper clientMapper;
    
    @Autowired
    private OAuthIssuer oAuthIssuer;

    public ClientDetails loadClientDetails(String clientId) {
        LOG.debug("Load ClientDetails by clientId: {}", clientId);
        return clientMapper.getClientDetailsByClientId(clientId);
    }


    private OAuthCode createAndSaveOauthCode(ClientDetails clientDetails,String username) throws OAuthSystemException {
        OAuthCode oAuthCode = new OAuthCode()
	        .setClientId(clientDetails.getClientId())
	        .setUsername(username)
	        .setCode(oAuthIssuer.authorizationCode());
        LOG.debug("Save authorizationCode '{}' of ClientDetails '{}'", oAuthCode, clientDetails);
        oAuthMapper.saveOAuthCode(oAuthCode);
        return oAuthCode;
    }
    
    public String retrieveAuthCode(ClientDetails clientDetails,String username) throws OAuthSystemException {
        final String clientId = clientDetails.getClientId();
        Map params = new HashMap();
        params.put("clientId", clientId);
        params.put("username", username);
        OAuthCode oAuthCode = oAuthMapper.getOAuthCode(params);
        if (oAuthCode != null) {
            //Always delete exist
            LOG.debug("OauthCode ({}) is existed, remove it and create a new one", oAuthCode);
            oAuthMapper.deleteOAuthCode(params);
        }
        //create a new one
        oAuthCode = createAndSaveOauthCode(clientDetails,username);

        return oAuthCode.getCode();
    }
    
//    private String currentUsername() {
//		// TODO Auto-generated method stub
//    	Authentication auth = SecurityContextHolder.getContext().getAuthentication();
//    	User userDetails=null;
//    	if (!(auth instanceof AnonymousAuthenticationToken)) {
//    		userDetails= (User) auth.getPrincipal();
//    	}
//    	return userDetails.getUsername();
//	}

	public OAuthCode loadOAuthCode(String code, ClientDetails clientDetails) {
        String clientId = clientDetails.getClientId();
        Map params = new HashMap();
        params.put("code", code);
        params.put("clientId", clientId);
        return oAuthMapper.getOAuthCode(params);
    }

    public boolean removeOAuthCode(String code, ClientDetails clientDetails) {
    	String clientId = clientDetails.getClientId();
    	Map params = new HashMap();
        params.put("code", code);
        params.put("clientId", clientId);
        int rows = oAuthMapper.deleteOAuthCode(params);
        return rows > 0;
    }
    
    public AccessToken retrieveAccessToken(ClientDetails clientDetails, Set<String> scopes, String username) throws OAuthSystemException {
        return retrieveAccessToken(clientDetails, scopes, username,clientDetails.supportRefreshToken());
    }

    public AccessToken retrieveAccessToken(ClientDetails clientDetails, Set<String> scopes, String username, boolean includeRefreshToken) throws OAuthSystemException {
        String scope = OAuthUtils.encodeScopes(scopes);
        String clientId = clientDetails.getClientId();

        String authenticationId = "";
		try {
			authenticationId = generateAuthenticationId(clientId, username, null);
		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		Map params = new HashMap();
        params.put("clientId", clientId);
        params.put("username", username);
        params.put("authenticationId", authenticationId);
        AccessToken accessToken = oAuthMapper.getAccessToken(params);
        if (accessToken == null) {
            accessToken = createAndSaveAccessToken(clientDetails, includeRefreshToken, username, authenticationId,scope);
            LOG.debug("Create a new AccessToken: {}", accessToken);
        }

        return accessToken;
    }
    
    
    public AccessToken retrieveNewAccessToken(ClientDetails clientDetails,String username,Set<String> scopes) throws OAuthSystemException {
    	String scope = OAuthUtils.encodeScopes(scopes);
        if(scope.isEmpty()){
        	scope = clientDetails.getScope();
        }
        String clientId = clientDetails.getClientId();
        String authenticationId = "";
		try {
			authenticationId = generateAuthenticationId(clientId, username, null);
		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		Map params = new HashMap();
        params.put("clientId", clientId);
        params.put("username", username);
        params.put("authenticationId", authenticationId);
        AccessToken accessToken = oAuthMapper.getAccessToken(params);
        if (accessToken != null) {
            LOG.debug("Delete existed AccessToken: {}", accessToken);
            oAuthMapper.deleteAccessToken(params);
        }
        accessToken = createAndSaveAccessToken(clientDetails, clientDetails.supportRefreshToken(), username, authenticationId,scope);
        LOG.debug("Create a new AccessToken: {}", accessToken);

        return accessToken;
	}
    
    
    //Always return new AccessToken
    public AccessToken retrieveNewAccessToken(ClientDetails clientDetails, OAuthCode oauthCode) throws OAuthSystemException {
    	String username = oauthCode.getUsername();
		return retrieveNewAccessToken(clientDetails,username,new HashSet<String>());
    }

    //grant_type=password AccessToken
    public AccessToken retrievePasswordAccessToken(ClientDetails clientDetails, Set<String> scopes, String username) throws OAuthSystemException {
        String scope = OAuthUtils.encodeScopes(scopes);
        if(scope.isEmpty()){
        	scope = clientDetails.getScope();
        }
        String clientId = clientDetails.getClientId();

        String authenticationId = "";
		try {
			authenticationId = generateAuthenticationId(clientId, username, scope);
		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		Map params = new HashMap();
        params.put("clientId", clientId);
        params.put("username", username);
        params.put("authenticationId", authenticationId);
        AccessToken accessToken = oAuthMapper.getAccessToken(params);

        boolean needCreate = false;
        if (accessToken == null) {
            needCreate = true;
            LOG.debug("Not found AccessToken from repository, will create a new one, client_id: {}", clientId);
        } else if (accessToken.tokenExpired()) {
            LOG.debug("Delete expired AccessToken: {} and create a new one, client_id: {}", accessToken, clientId);
            oAuthMapper.deleteAccessToken(params);
            needCreate = true;
        } else {
            LOG.debug("Use existed AccessToken: {}, client_id: {}", accessToken, clientId);
        }

        if (needCreate) {
            accessToken = createAndSaveAccessToken(clientDetails, clientDetails.supportRefreshToken(), username, authenticationId,scope);
            LOG.debug("Create a new AccessToken: {}", accessToken);
        }

        return accessToken;

    }


    //grant_type=client_credentials
    public AccessToken retrieveClientCredentialsAccessToken(ClientDetails clientDetails, Set<String> scopes) throws OAuthSystemException {        
        return retrievePasswordAccessToken(clientDetails, scopes, clientDetails.getClientId());
    }
    
    public AccessToken loadAccessTokenByToken(String token) {
    	LOG.debug("Load AccessToken by token: {}", token);
        if(token.isEmpty()){
        	return null;
        }
    	Map params = new HashMap();
        params.put("token", token);
        return oAuthMapper.getAccessToken(params);
    }

    public AccessToken loadAccessTokenByRefreshToken(String refreshToken, String clientId) {
        LOG.debug("Load AccessToken by refreshToken: {} and clientId: {}", refreshToken, clientId);
        Map params = new HashMap();
        params.put("clientId", clientId);
        params.put("refreshToken", refreshToken);
        return oAuthMapper.getAccessToken(params);
    }

    /*
    * Get AccessToken
    * Generate a new AccessToken from existed(exclude token,refresh_token)
    * Update access_token,refresh_token, expired.
    * Save and remove old
    * */
    public AccessToken changeAccessTokenByRefreshToken(String refreshToken, String clientId) throws OAuthSystemException {
        AccessToken oldToken = loadAccessTokenByRefreshToken(refreshToken, clientId);

        AccessToken newAccessToken = oldToken.cloneMe();
        LOG.debug("Create new AccessToken: {} from old AccessToken: {}", newAccessToken, oldToken);

        ClientDetails clientDetails = clientMapper.getClientDetailsByClientId(clientId);

        String authenticationId = "";
		try {
			authenticationId = generateAuthenticationId(clientId, oldToken.getUsername(), null);
		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
        newAccessToken.setAuthenticationId(authenticationId)
                .setToken(oAuthIssuer.accessToken())
                .setRefreshToken(oAuthIssuer.refreshToken());
        Map params = new HashMap();
        params.put("clientId", oldToken.getClientId());
        params.put("username", oldToken.getUsername());
        params.put("authenticationId", oldToken.getAuthenticationId());
        oAuthMapper.deleteAccessToken(params);
        LOG.debug("Delete old AccessToken: {}", oldToken);

        oAuthMapper.saveAccessToken(newAccessToken);
        LOG.debug("Save new AccessToken: {}", newAccessToken);

        return newAccessToken;
    }

    private AccessToken createAndSaveAccessToken(ClientDetails clientDetails, boolean includeRefreshToken, String username, String authenticationId, String scope) throws OAuthSystemException {
        AccessToken accessToken = new AccessToken()
                .setClientId(clientDetails.getClientId())
                .setUsername(username)
                .setToken(oAuthIssuer.accessToken())
                .setAuthenticationId(authenticationId)
                .setScope(scope);

        if (includeRefreshToken) {
            accessToken.setRefreshToken(oAuthIssuer.refreshToken());
        }
        LOG.debug("Save accessToken '{}' of ClientDetails '{}'", accessToken, clientDetails);
        oAuthMapper.saveAccessToken(accessToken);
        LOG.debug("Save AccessToken: {}", accessToken);
        return accessToken;
    }

//    private OAuthCode createOauthCode(ClientDetails clientDetails) throws OAuthSystemException {
//        OAuthCode oAuthCode;
//        String authCode = oAuthIssuer.authorizationCode();
//
//        LOG.debug("Save authorizationCode '{}' of ClientDetails '{}'", authCode, clientDetails);
//        oAuthCode = this.saveAuthorizationCode(authCode, clientDetails);
//        return oAuthCode;
//    }

    private String generateAuthenticationId(String clientId, String username, String scope) throws NoSuchAlgorithmException {
        Map<String, String> map = new HashMap<String, String>();
        map.put(OAuth.OAUTH_CLIENT_ID, clientId);
        //check it is client only
        if (!clientId.equals(username)) {
            map.put(OAuth.OAUTH_USERNAME, username);
        }
        if (!OAuthUtils.isEmpty(scope)) {
            map.put(OAuth.OAUTH_SCOPE, scope);
        }

        return CommonUtils.getMD5(map.toString());
    }

	public boolean checkAccessToken(HttpServletRequest request, HttpServletResponse response) {
		// TODO Auto-generated method stub
		String token = request.getParameter(OAuth.OAUTH_ACCESS_TOKEN)==null?"":request.getParameter(OAuth.OAUTH_ACCESS_TOKEN);
		//请求的是哪个资源，不能为空
		String rsname = request.getParameter("rsname")==null?"":request.getParameter("rsname");
		String scope = request.getParameter("scope")==null?"":request.getParameter("scope");
		AccessToken accessToken = loadAccessTokenByToken(token);
		try{
			if(rsname.isEmpty()){
				OAuthResponse oAuthResponse = OAuthRSResponse
	                    .errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
	                    .setRealm("Apache Oltu")
	                    .setError("Rsname is null")
	                    .buildJSONMessage();
				CommonUtils.writeOAuthJsonResponse(response, oAuthResponse);
				return false;
			}
						
			if(accessToken == null){
				OAuthResponse oAuthResponse = OAuthRSResponse
	                    .errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
	                    .setRealm("Apache Oltu")
	                    .setError(OAuthError.ResourceResponse.INVALID_TOKEN)
	                    .buildJSONMessage();
				CommonUtils.writeOAuthJsonResponse(response, oAuthResponse);
				return false;
			}else{
				boolean scopeValid = false;
				String tokenScopes = accessToken.getScope();
				if(tokenScopes.isEmpty()){
					scopeValid = true;
				}else{
					Map<String, Set<String>> allowedScope = new HashMap<String, Set<String>>();
			        for(String tokenScope:tokenScopes.split(";")){
			        	Set<String> value = new HashSet<String>();
			        	if(tokenScope.split("@").length > 1){
			        		CollectionUtils.addAll(value,tokenScope.split("@")[1].split(","));
			        	}
			        	allowedScope.put(tokenScope.split("@")[0],value);		        	
			        	if(allowedScope.containsKey(rsname)){
			        		if(allowedScope.get(rsname).isEmpty()){
			        			scopeValid = true;
			        		}else{
				        		for(String s:allowedScope.get(rsname)){
				        			if(scope.startsWith(s)) scopeValid = true;
				        		}
			        		}
			        	}
			        }
				}
		        if(!scopeValid){
	        		OAuthResponse oAuthResponse = OAuthResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
			                .setError(OAuthError.CodeResponse.INVALID_SCOPE)
			                .setErrorDescription("Invalid scope '" + rsname + "@" + scope + "'")
			                .buildJSONMessage();
	        		CommonUtils.writeOAuthJsonResponse(response, oAuthResponse);
	        		return false;
	        	}
			}
		} catch (Exception e) {
	         e.printStackTrace();
		}
		return true;
	}

}
